Analyse : Concevez une application au service de la santé publique

  • Projet-3 : Analyse des données
  • IDEE APPLICATION
  • 1. Chargement du jeu de données (dataset en anglais)
  • Analyse des dates de création et modification des produits
  • ANALYSE UNIVARIEE
    • Quantitatives continues
    • Variable quantitative discrete : Le nutriscore est-il bien distribué, temoignant du repartition équilibrée des produits ?
      • 'nutrition-score-fr_100g'
    • Les variables qualitatives
      • Variables qualitatives nominales
      • Qui sont les sources de ces données ?
      • Les marques sont_elles disponibles sur le marché français ?
      • Catégories
      • Les additifs à surveillées sont ils des informations dont nous pourrions disposer ?
        • Additifs ciblés
    • Variables qualitatives ordinales
      • Répartition des nutrition_grades et l'application
  • ANALYSE BIVARIEES
    • Les corrélations
    • Fonctions de test pour les analyses
      • Tester la normalité
    • Nutrigrade | targets
      • Nutrigrade | Nutriscore
      • Nutrigrade | Protéines
      • Nutrigrade | Sodium
    • Produits | targets
    • Categoiries | targets
  • ANALYSE MULTIVARIEE
    • Regression linéaire multiple
    • Analyse en Composantes Principales (ACP)
    • Clustering
  • BILAN

Projet-3 : Analyse des données¶

In [1]:
import pandas as pd
import numpy as np
import datetime as dt
import sys
import warnings
import IPython as ip
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
from wordcloud import WordCloud
import scipy.stats as st
from scipy.stats import t, shapiro
from scipy.stats import normaltest
import statsmodels 
import statsmodels.api as sm 
import statsmodels.formula.api as smf
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.graphics.gofplots import qqplot
# ACP
from sklearn.preprocessing import StandardScaler
from sklearn import decomposition
from sklearn.decomposition import PCA
from sklearn import decomposition
from sklearn import preprocessing


from IPython.display import display
import missingno as msno 
# Configuration pour travail avec fichier python "tools" de fonctions
%load_ext autoreload
%aimport tools
# Recharger les modules pour la conception des fichiers tools
%autoreload 1

# Set option
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)


warnings.filterwarnings("ignore")

IDEE APPLICATION¶


  • L'application doit permettre aux personnes qui recherchent des conseils alimentaires en cas de maladie rénale chronique de trouver des informations suppleméntaires (info visuel) qui est spécifique au gens qui doivent préserver au mieux leurs reins (problème rénal).
  • L'application ne remplace pas un régime spécifique fait par un professionnel de santé mais constitue une aide à a décision par une information simplifiée concernant les indicateur qui sont importants de surveiller lorsqu'on doit surveiller son alimentation pour préserver ses reins.

  • Objectifs : COMMENT PROTEGER MES REINS ?
    • Limiter l’apport en sel
    • Contrôler les apports en protéines,
    • Couvrir les besoins nutritionnels.

On surveille :

  • Le poids : alimentation saine, équilibrée et plaisir :
    • 'nutrition-score-fr_100g','energy_100g'
    • 'fat_100g', 'saturated-fat_100g','carbohydrates_100g', 'sugars_100g', 'fiber_100g'
  • Les indicateurs qui nous seraient utiles :
    • Sel('salt_100g')
    • Sodium('sodium_100g') : sel minéral qui intervient dans l’équilibre hydrique du corps
      • 1g de sel (NaCl) équivaut à 400 mg de sodium (Na)
      • 1 gramme de sodium(Na) correspond donc à 2.5 g de sel
    • Proteines ('proteins_100g') : Au stade d’insuffisance rénale les besoins en protéines sont de 0,8g/Kg/j
      • en fonction du poids :
        • -60kg => 40g
        • 60kg => 46g
        • 70kg => 56g
        • 80kg => 64g
    • Le potassium est un minéral important présent dans un grand nombre d’aliments indispensables au bon fonctionnement des muscles et du coeur. : trop de données manquantes
    • Le phosphore est surtout présent dans les aliments, lié aux protéines.
      • La limitation protéique entraîne déjà une diminution des apports en phosphore.
    • Limiter les additifs :
      • E 338 Acide phosphorique (boisson au cola)
      • E 339 Phosphates de sodium
      • E 340 Phosphates de potassium
      • E 341 Phosphates de calcium
      • E 343 Phosphates de magnésium
      • E 450 Diphosphates
      • E 451 Triphosphates
      • E 452 Polyphosphates

Problématique : Les données du jeu de données peuvent-elles répondre aux objectifs ?


1. Chargement du jeu de données (dataset en anglais)¶

In [2]:
# Import données
data = pd.read_csv('assets/datas/df_app_knnImputer.csv', sep='\t',parse_dates=[2,3], low_memory=False)
In [3]:
df = data.copy()
# Visualisation d'un échantillon de la population
df.sample(5)
Out[3]:
code creator created_datetime last_modified_datetime product_name brands categories_fr countries_fr additives_n additives_fr ingredients_from_palm_oil_n nutrition_grade_fr main_category_fr energy_100g fat_100g saturated_fat_100g carbohydrates_100g sugars_100g fiber_100g proteins_100g salt_100g sodium_100g nutrition_score_fr_100g
130072 0725433910879 usda-ndb-import 2017-03-10 09:00:26 2017-03-10 09:00:26 Freezer Pops My Essentials inconnu États-Unis 4.0 E330 - Acide citrique,E133 - Bleu brillant FCF... 0.0 c inconnu 297.0 0.00 0.00 17.86 17.86 0.0 0.00 0.00000 0.000 3.0
64189 0054500193540 usda-ndb-import 2017-03-09 16:41:23 2017-03-09 16:41:23 Park's Finest, Beef Frankfurters, Slow Cooked ... Ball Park, Sara Lee Foods inconnu États-Unis 1.0 E339iii - Phosphate de sodium tribasique 0.0 e inconnu 1322.0 26.32 10.53 7.02 1.75 0.0 12.28 3.25374 1.281 23.0
98672 0083737146517 usda-ndb-import 2017-03-09 11:51:03 2017-03-09 11:51:03 Jasmine Rice Vera inconnu États-Unis 0.0 0.0 b inconnu 1490.0 0.00 0.00 80.00 0.00 0.0 6.67 0.00000 0.000 0.0
201715 3301594000726 openfoodfacts-contributors 2016-02-25 11:17:12 2017-03-24 14:35:47 La Compote de Fraises avec Morceaux Côteaux Nantais Aliments et boissons à base de végétaux,Alimen... France 0.0 0.0 a Desserts 443.0 0.00 0.00 23.60 21.40 1.9 0.70 0.00000 0.000 -2.0
156212 0854171004363 usda-ndb-import 2017-03-09 21:08:05 2017-03-09 21:08:05 Macadamia Milk Royal Hawaiian Macadamia Nut Inc. inconnu États-Unis 4.0 E322 - Lécithines,E414 - Gomme d'acacia,E170 -... 0.0 b inconnu 63.0 1.46 0.21 0.42 0.00 0.4 0.00 0.10668 0.042 0.0
In [4]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 258542 entries, 0 to 258541
Data columns (total 23 columns):
 #   Column                       Non-Null Count   Dtype         
---  ------                       --------------   -----         
 0   code                         258542 non-null  object        
 1   creator                      258542 non-null  object        
 2   created_datetime             258542 non-null  datetime64[ns]
 3   last_modified_datetime       258542 non-null  datetime64[ns]
 4   product_name                 258542 non-null  object        
 5   brands                       258542 non-null  object        
 6   categories_fr                258542 non-null  object        
 7   countries_fr                 258542 non-null  object        
 8   additives_n                  258542 non-null  float64       
 9   additives_fr                 258542 non-null  object        
 10  ingredients_from_palm_oil_n  258542 non-null  float64       
 11  nutrition_grade_fr           258542 non-null  object        
 12  main_category_fr             258542 non-null  object        
 13  energy_100g                  258542 non-null  float64       
 14  fat_100g                     258542 non-null  float64       
 15  saturated_fat_100g           258542 non-null  float64       
 16  carbohydrates_100g           258542 non-null  float64       
 17  sugars_100g                  258542 non-null  float64       
 18  fiber_100g                   258542 non-null  float64       
 19  proteins_100g                258542 non-null  float64       
 20  salt_100g                    258542 non-null  float64       
 21  sodium_100g                  258542 non-null  float64       
 22  nutrition_score_fr_100g      258542 non-null  float64       
dtypes: datetime64[ns](2), float64(12), object(9)
memory usage: 45.4+ MB
In [5]:
tools.get_description_variables(df,type_var='categ')
Out[5]:
count unique top freq first last
code 258542 258542 0000000004530 1 NaT NaT
creator 258542 2507 usda-ndb-import 169205 NaT NaT
created_datetime 258542 129878 2017-03-09 16:32:00 20 2012-01-31 14:43:58 2017-04-20 21:13:06
last_modified_datetime 258542 122969 2015-08-09 17:35:48 24 2012-04-08 08:12:35 2017-04-21 00:53:41
product_name 258542 186896 Ice Cream 410 NaT NaT
brands 258542 46267 inconnue 3353 NaT NaT
categories_fr 258542 16466 inconnu 194668 NaT NaT
countries_fr 258542 83 États-Unis 171001 NaT NaT
additives_fr 258542 39839 109320 NaT NaT
nutrition_grade_fr 258542 6 d 61860 NaT NaT
main_category_fr 258542 2353 inconnu 194668 NaT NaT
  • Les types de données sont bons
  • Le nutrigrade_fr 'd' est le plus représenté
  • La catégorie qui revein le plus est 'Ice Cream'
  • Le pays le plus représenté est : les États-Unis
In [6]:
tools.get_description_variables(df,type_var='num')
Out[6]:
count mean std min 25% 50% 75% max
additives_n 258542.0 1.801626 2.463613 0.0 0.0000 1.000000 3.000000 31.000000
ingredients_from_palm_oil_n 258542.0 0.016972 0.130774 0.0 0.0000 0.000000 0.000000 2.000000
energy_100g 258542.0 1121.262090 791.479058 0.0 377.0000 1100.000000 1674.000000 3776.000000
fat_100g 258542.0 12.224777 16.939989 0.0 0.0000 4.710000 19.571500 100.000000
saturated_fat_100g 258542.0 4.564584 7.493563 0.0 0.0000 1.190000 6.670000 100.000000
carbohydrates_100g 258542.0 31.484738 28.505266 0.0 6.2500 20.350000 57.000000 100.000000
sugars_100g 258542.0 15.350445 20.808651 0.0 1.0600 5.130000 22.580000 100.000000
fiber_100g 258542.0 2.457956 4.179495 0.0 0.0000 1.100000 3.300000 100.000000
proteins_100g 258542.0 7.076648 8.135777 0.0 0.7000 4.760000 10.000000 100.000000
salt_100g 258542.0 1.583596 6.194799 0.0 0.0635 0.586740 1.370000 100.000000
sodium_100g 258542.0 0.624671 2.441285 0.0 0.0250 0.230715 0.538937 39.370079
nutrition_score_fr_100g 258542.0 8.965165 8.777342 -15.0 1.0000 9.000000 15.800000 40.000000
  • La repartion interquartille ne montre pas un désequilibre marquant mais il faut approfondit les éléments qui nous interesse et notament energy_fr, nutrition_score

ANALYSE UNIVARIEE¶


Analyse des dates de création et modification des produits¶

In [7]:
add_per_year = df['code'].groupby(by=df['created_datetime'].dt.year).nunique()
modified_per_year = df['code'].groupby(by=df['last_modified_datetime'].dt.year).nunique()

fig=plt.figure(figsize=(12,8))

font_title = {'family': 'serif',
              'color':  '#114b98',
              'weight': 'bold',
              'size': 18,
             }

sns.set_style("whitegrid")
plt.plot(add_per_year, 
         color="#114b98", 
         label="Ajouts")
plt.plot(modified_per_year, 
         color="#00afe6", 
         label="Modifications")
plt.title("Evolution des créations et modifications de produits par année", 
          fontdict=font_title)
plt.xlabel("Année")
plt.ylabel("Nombre de produits")
plt.legend()
plt.savefig("assets/graphiques/Evolutions_dates.jpg")

plt.show()

Bilan date : Le pic de 2016, le début d'une collecte massive

  • Explication des dates :
    • Le Nutri-Score est prévu dans la loi de 2016 en France
    • Mis en place en France en 2017, l'étiquetage nutritionnel Nutri-Score s'applique aujourd'hui dans sept pays
In [8]:
# Variables numériques On écarte les dates
cols_num = df.select_dtypes(include=[np.number]).columns.to_list()
# Variables quantitatives discrètes
cols_quant_discr = ['additives_n','ingredients_from_palm_oil_n','nutriscore_score_fr']
# Variables quantitatives continue
cols_quant_cont = ['energy_100g', 'fat_100g','saturated_fat_100g', 
                   'carbohydrates_100g', 'sugars_100g', 'fiber_100g',
                   'proteins_100g', 'salt_100g', 'sodium_100g']

Les variables qualitatives¶

In [9]:
# On visualise le nombre de valeurs uniques contenu dans les colonnes de type object
for col in df.select_dtypes('object'):
    print(f'{col:-<50} {df[col].nunique()}')
code---------------------------------------------- 258542
creator------------------------------------------- 2507
product_name-------------------------------------- 186896
brands-------------------------------------------- 46267
categories_fr------------------------------------- 16466
countries_fr-------------------------------------- 83
additives_fr-------------------------------------- 39839
nutrition_grade_fr-------------------------------- 6
main_category_fr---------------------------------- 2353
In [10]:
# Variables qualitatives ou modalités
# Variables qualitatives nominales
cols_qual_nom = ['code','creator','product_name','brands', 
                 'categories_fr','main_category_fr', 'countries_fr','additives_fr']
In [11]:
def top_N_pie (df,var,name,n,taille,perc) : 
    ''' 
    Fonction qui visualise les n plus grand d'une colonne avec ou sans pourcentage
    parametres : 
        df
        var : colonne ciblée
        name : 'nom de la colonne '
        n : nombre de top voulu
        taille : taille du pieplot
        per : si True : affiche les pourcentages
    '''
    target = df.groupby(by=var)['code'].nunique().sort_values(ascending=False)
    # Graphiques top N
    
    fig, ax = plt.subplots(figsize=(taille, taille), subplot_kw=dict(aspect="equal"))
    explodes = np.zeros(n)
    explodes[0] = .1
    # calcul des pourcentages
    if perc: 
        def pct_tot(pct):
            tot = round(pct*target[:n].sum(),0)
            tot_pct = tot/target.sum()
            return "{:.1f}%\n({:.0f})".format(tot_pct,(tot/100))
        plt.pie(target[:n], labels=target[:n].index,
            startangle=45,
            shadow=True,
            autopct=lambda pct: pct_tot(pct),
            explode=explodes,
            textprops=dict(color="black",size=12, weight="bold"))
    else : 
        plt.pie(target[:n], labels=target[:n].index,
                startangle=45,
                shadow=True,
                explode=explodes,
                textprops=dict(color="black",size=10, weight="bold"))
    plt.title(f"TOP {n} : {name}",fontweight='bold',fontsize=24)
    plt.show()

Qui sont les sources de ces données ?¶

In [12]:
# Nombre de créateurs, sources des données
print(f"Nombre de sources unique : {df['creator'].nunique()}")
Nombre de sources unique : 2507
In [13]:
top_N_pie(df,'creator','Contributeurs',5,12,True)
plt.savefig("assets/graphiques/Top_Contributeurs.jpg")
<Figure size 640x480 with 0 Axes>
  • 2478 contributeurs dont des institutions internationales mais aussi des particuliers
  • La question de la fiabilité des données se pose dès les 6 plus gros contributeurs
    • La majorité des données provient de l'USDA : Misitère de l'agriculture américain : https://www.usda.gov/topics/trade/importing-goods
    • openfoodfacts-contributors : Communauté de contributeurs : https://world.openfoodfacts.org/contribute
    • kiliweb : ??
    • openfood-ch-import : sûrement une instition suisse mais pas de certitudes
    • date-limie-app : ??
    • Tacite : contributeur pernonnel ?

Les marques sont_elles disponibles sur le marché français ?¶

  • Trouve t-on dans le jeu de données les marques qui sont présentent sur le marché français, consommateurs cible de notre application ?
In [14]:
df['brands'].nunique()
Out[14]:
46267
In [15]:
# On s'occupe ici uniquement des catégories renseignées
df_brands = df[~(df['brands']=='inconnue')]
df_brands.shape
Out[15]:
(255189, 23)
In [16]:
# Tableau fréquences
dico = df_brands.groupby('brands')['brands'].count().sort_values(ascending=False).to_dict()
nom = 'brands'
col1 = 'Nom_' + nom
col2 = 'Nbr_' + nom
col3 = 'Fréquence (%)'
df_gpe = pd.DataFrame(dico.items(), columns=[col1, col2])
df_gpe[col3] = (df_gpe[col2] * 100) / len(df_brands)
df_gpe.head(10)
Out[16]:
Nom_brands Nbr_brands Fréquence (%)
0 Carrefour 2506 0.982017
1 Meijer 1990 0.779814
2 Auchan 1933 0.757478
3 U 1754 0.687334
4 Kroger 1656 0.648931
5 Leader Price 1411 0.552924
6 Ahold 1368 0.536073
7 Spartan 1337 0.523925
8 Casino 1294 0.507075
9 Roundy's 1293 0.506683
In [17]:
# Wordecloud
from wordcloud import WordCloud

wordcloud = WordCloud(width=800,height=400, background_color="white",max_words=100).generate_from_frequencies(dico)
plt.figure(figsize=(12, 10))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()
  • Sur 46265 marques différentes dans le df
    • 237159 produit non renseignés : 'inconnues'
  • Les marques disponibles en France sont bien représentées :
    • Carrefour, Auchan, Leader Price, U, Casino, Monoprix, Picard, Bjorg, Nestlé, Lipton, Cora, Harris... sont des marques populaires en France
In [18]:
 df_gp_red = df_gpe.head(10)
sns.set_style("whitegrid")
plt.figure(figsize=(8, 4))
sns.barplot(
    y=df_gp_red[col1],
    x=df_gp_red[col3],
    data=df_gp_red,
    color='SteelBlue')
plt.title('Répartition de la présence des marques dans le jeu de données')
plt.grid(False)
plt.tight_layout()
plt.show()

Quelles sont les catégories les plus représentés ?¶

In [19]:
df['categories_fr'].nunique()
Out[19]:
16466
In [20]:
# On s'occupe ici uniquement des catégories renseignées
df_categ = df[~(df['categories_fr']=='inconnu')]
In [21]:
tools.affiche_wordcloud_tabfreq(df_categ,'categories_fr','categories',)
Nom_categories Nbr_categories Fréquence (%)
Snacks sucrés,Biscuits et gâteaux,Biscuits 706 1.105301
Snacks sucrés,Chocolats,Chocolats noirs 538 0.842283
Aliments et boissons à base de végétaux,Aliments d'origine végétale,Petit-déjeuners,Céréales et pommes de terre,Céréales et dérivés,Céréales pour petit-déjeuner 479 0.749914
Snacks sucrés,Biscuits et gâteaux,Biscuits,Biscuits au chocolat 421 0.659110
Snacks salés,Apéritif,Biscuits apéritifs 410 0.641889
Snacks sucrés,Confiseries,Bonbons 396 0.619971
Snacks sucrés,Chocolats,Chocolats au lait 383 0.599618
Produits laitiers,Yaourts 356 0.557347
Snacks sucrés,Chocolats 324 0.507249
Epicerie,Sauces 304 0.475937
In [ ]:
 
In [22]:
tools.affiche_wordcloud_tabfreq(df_categ,'main_category_fr','Main categories',affword=True)
Nom_Main categories Nbr_Main categories Fréquence (%)
Epicerie 2412 3.776184
Boissons 2404 3.763660
Chocolats 2343 3.668159
Aliments et boissons à base de végétaux 2334 3.654069
Conserves 2153 3.370699
Biscuits 1892 2.962082
Plats préparés 1843 2.885368
Surgelés 1738 2.720982
Petit-déjeuners 1655 2.591039
Snacks sucrés 1587 2.484579
  • Parmi les catégories les plus représentés ont retrouve beaucoup de produits considérés comme à surveiller dans une alimentation saine.
  • A défaut d'une sur représentation des produits sain on peut exploiter ces informationspour informer le consommateur sur les produits à surveiller. Une bonne alimentation passe aussi par le plaisir et ne doit pas être stigmatisé sans avis médical personnalisé contraire.

  • Pour notre appli cette source de données est importantes et l'application devra signaler les choses de manière pédagogique

    • Alimentation : interdite par le medecin
    • alimentation : apport à surveiller
    • alimentation : recommandé

Les additifs à surveillées sont ils des informations dont nous pourrions disposer ?¶


Additifs ciblés


  • Limiter les additifs :
     - E 338 Acide phosphorique (boisson au cola)
     - E 339 Phosphates de sodium
     - E 340 Phosphates de potassium
     - E 341 Phosphates de calcium
     - E 343 Phosphates de magnésium
     - E 450 Diphosphates
     - E 451 Triphosphates
     - E 452 Polyphosphates
In [23]:
# On s'occupe ici uniquement des catégories renseignées
df_additives = df[~(df['additives_fr']==' ')]
In [24]:
df_additives_target = df_additives.copy()
df_additives_target = df_additives_target[df_additives_target['additives_fr'].str.contains("338|339|340|341|343|450|451|452")]
In [25]:
tools.affiche_wordcloud_tabfreq(df_additives_target,'additives_fr','Additives',affword=False)
Nom_Additives Nbr_Additives Fréquence (%)
E452vi - Tripolyphosphate de sodium et de potassium 396 1.659125
E339iii - Phosphate de sodium tribasique,E316 - Erythorbate de sodium,E250 - Nitrite de sodium 321 1.344897
E339iii - Phosphate de sodium tribasique 302 1.265292
E339 - Orthophosphates de sodium 233 0.976202
E450 - Sels métalliques de diphosphates 207 0.867270
E339 - Orthophosphates de sodium,E316 - Erythorbate de sodium,E250 - Nitrite de sodium 207 0.867270
E325 - Lactate de sodium,E339 - Orthophosphates de sodium,E262ii,E316 - Erythorbate de sodium,E250 - Nitrite de sodium 199 0.833752
E325 - Lactate de sodium,E339iii - Phosphate de sodium tribasique,E262ii,E316 - Erythorbate de sodium,E250 - Nitrite de sodium 186 0.779286
E375 - Acide nicotinique,E101 - Riboflavine,E450 - Sels métalliques de diphosphates 177 0.741579
E341iii - Phosphate de tricalcium 150 0.628457
Les addfitifs que nous ciblons sont dans la base de données
  • Les addfitifs que nous ciblons sont dans la base de données

Les informations nutrionnelles répondent-elles aux enjeux du projet ?¶

In [26]:
# Mesures de tendances centrales des colonnes quantitatives continues
tools.stat_descriptives(df,cols_quant_cont)
Out[26]:
Desc energy_100g fat_100g saturated_fat_100g carbohydrates_100g sugars_100g fiber_100g proteins_100g salt_100g sodium_100g
mean 1121.262090 12.224777 4.564584 31.484738 15.350445 2.457956 7.076648 1.583596 0.624671
median 1100.000000 4.710000 1.190000 20.350000 5.130000 1.100000 4.760000 0.586740 0.230715
var 626436.675978 286.962109 56.153266 812.547053 432.998278 17.468109 66.190611 38.375388 5.959852
std 791.477527 16.939956 7.493548 28.505211 20.808611 4.179487 8.135761 6.194787 2.441281
skew 0.436897 2.225307 3.494783 0.608215 1.734052 5.333083 2.125908 11.111269 11.084890
kurtosis -0.442036 6.439908 22.343529 -0.964591 2.509134 57.404463 8.419095 142.775439 142.212061
mode 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0
Min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
Max 3776.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 39.370079

Mediane et moyenne sont écartées

  • Skewness : coefficient d'asymetrie (mesure de l'asymétrie)
    • energy_100g et carbohydrate_100g semble se rapprocher d'une distribution symétrique (skew proche de 0)
    • sugars_100g fat_100g saturated_fat_100g fiber_100g proteins_100g salt_100g sodium_100g (skew > 0)

  • kurtosis : coefficient d'applatissement
    • Un kurtosis élevé saturated_fat_100g,fiber_100g,salt_100g,sodium_100g , indique que la distribution est plutôt pointue. kurtisis > 3 on parle de distribution leptokurtique
    • À l'opposé, un kurtosis proche de zéro indique une distribution relativement aplatie pour une même variance. kurtosis < 3, on parlera de distribution platikurtique pour energy_100g, carbohydrates_100g.
    • Si kurtosis = 3, sugars_100g de distribution mesokurtique (kurtosis de la forme normale)

In [27]:
def var_hist(var, i):
    subset = df[var]
    n_df_valide = len(df)
    xbar = np.mean(df[var]) # Moyenne
    sprime = np.std(df[var], ddof=1) # Ecart-type
    sprime2 = np.var(df[var], ddof=1) #Variance non biaisée
    
    ax = fig.add_subplot(i)
    ax.hist(subset, density=True)
    ax.axvline(xbar, color='r', linewidth=2, label="Moyenne empirique")
    bins = np.arange(df[var].min(),df[var].max(),0.05)
    y = st.norm.pdf(bins, xbar, sprime)
    ax.plot(bins, y, '--', label="Densité normale")
    ax.legend()
    ax.set_xlabel(var, fontsize=12)
    ax.set_ylabel('Densité', fontsize=12)
    ax.set_title('Distribution de '+str(var), fontsize=18)
In [28]:
liste_var = cols_quant_cont
plt.style.use('seaborn-whitegrid')
fig = plt.figure(figsize=(20,30),constrained_layout=False)
i = 331
for var in liste_var :
    var_hist(var, i)
    i+=1
plt.savefig("assets/graphiques/analyse univariee histo_dfComplet.jpg")
In [29]:
for column in cols_quant_cont:
    plt.figure(figsize = (10,2))
    sns.set(font_scale=1)
    sns.distplot(data[column], bins=50)
    chaine = 'Distribution de : ' + column
    plt.title(chaine)
    plt.xlabel(column)
    plt.show()
In [30]:
df['energy_100g'].describe()
Out[30]:
count    258542.000000
mean       1121.262090
std         791.479058
min           0.000000
25%         377.000000
50%        1100.000000
75%        1674.000000
max        3776.000000
Name: energy_100g, dtype: float64
In [31]:
df['sodium_100g'].describe()
Out[31]:
count    258542.000000
mean          0.624671
std           2.441285
min           0.000000
25%           0.025000
50%           0.230715
75%           0.538937
max          39.370079
Name: sodium_100g, dtype: float64
In [32]:
df['proteins_100g'].describe()
Out[32]:
count    258542.000000
mean          7.076648
std           8.135777
min           0.000000
25%           0.700000
50%           4.760000
75%          10.000000
max         100.000000
Name: proteins_100g, dtype: float64
In [ ]:
 
In [ ]:
 
In [33]:
# Representation graphique des outliers:
a = 3  # nombre de lignes
b = 3  # nombre de colonnes
c = 1  # initialisation
fig = plt.figure(figsize=(20,8))

for i in df.loc[:, cols_quant_cont]: # pour toute les colonnnes quantatives
    plt.subplot(a, b, c) # maillage des subplot
    plt.title('{} (boxplot)'.format(i, a, b, c))# titres des box plot
    plt.xlabel(i) # xlabel = nom de la colonne
    sns.boxplot(x = df[i]) # faire un boxplot sns
    c = c + 1 # incrementation ==> création d'un nouveau box plot
plt.subplots_adjust(left=0.125, # gerer les espacements
                    bottom=0.1, 
                    right=0.9, 
                    top=0.9, 
                    wspace=0.2, 
                    hspace=0.35)
plt.savefig("assets/graphiques/analyse univariee boxplot valeurs quant.jpg")

Bilan :


  • Toutes les variables quantitatives continues sont asymétriques avec un skewness montrant un étalement vers la droite
  • Un pic de valeurs autour de 0 pour toutes les variables (concernent des produits qui ne contiennent pas ces éléments en effet un produit peut ne pas contenir de sel... ce ne sont pas forcement des erreurs)
  • Si l'on except le sucre aucun des Kurtosis de nos variable quantitatives continues n'est en adéquation avec une distribution qui suit la forme normale
  • Sodium, proteines et nutriscore voir points dédiés plus bas
  • Des distributions concentrées autour de zero
  • Des formes similaires avec une asymétrie à gauche et une queue à droite
  • Des formes qui ne suivent pas une loi normale
  • Tester la normalité

Le test de Shapiro-Wilk est un test de normalité sur de petits échantillons. Il est utilisé pour déterminer si un échantillon provient ou non d'une distribution normale.

Hypothèses :

  • H0 : la distribution des données est normale (P>0,05)
  • H1 : la distribution des données n'est pas normale (P<0,05)

Comme la valeur p est inférieure à 0,05, nous rejetons l'hypothèse nulle. les données de l'échantillon ne proviennent pas d'une distribution normale.


normaltest basé sur D'Agostino an Pearson's

Hypothèses :

  • H0 : la distribution des données est normale (P>0,05)
  • H1 : la distribution des données n'est pas normale (P<0,05)

Création d'un fonction pour faire les tests

In [34]:
def test_normalite(df,var):
    ''' Création d'une fonction pour tester la normalioté de la distribution des variables
    '''
    # print(f"Variable : {var}")
    stat, p = shapiro(df[var]) 
    print ('Test Shapiro-wilk')
    print('stat=%.3f\np=%.3f' % (stat,p))
    #interprétation
    if p > 0.05:
        print("Distribution probablement gaussienne")
    else:
        print("Distribution non Gaussienne")
    print("------------------------------------------")
    print("Test normaltest (d'Agostino)")
    stat, p = normaltest(df[var])
    print('Statistics=%.3f, p=%.3f' % (stat, p))
# interpretation
    alpha = 0.05
    if p < alpha:  # null hypothesis: x comes from a normal distribution
        print("La distribution ne suit pas la loi normale (P<0,05) ")
    else:
        print("La distribution suit une loi normale (P>0,05)")
    print("________________________________________________________")
In [35]:
for col in cols_quant_cont:
    print(f"colonne : {col}")
    test_normalite(df,col)
colonne : energy_100g
Test Shapiro-wilk
stat=0.952
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=11099.802, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________
colonne : fat_100g
Test Shapiro-wilk
stat=0.735
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=119816.694, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________
colonne : saturated_fat_100g
Test Shapiro-wilk
stat=0.642
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=199678.033, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________
colonne : carbohydrates_100g
Test Shapiro-wilk
stat=0.888
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=62938.887, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________
colonne : sugars_100g
Test Shapiro-wilk
stat=0.747
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=79676.682, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________
colonne : fiber_100g
Test Shapiro-wilk
stat=0.580
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=279188.641, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________
colonne : proteins_100g
Test Shapiro-wilk
stat=0.800
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=122104.184, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________
colonne : salt_100g
Test Shapiro-wilk
stat=0.192
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=421476.943, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________
colonne : sodium_100g
Test Shapiro-wilk
stat=0.193
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=420952.198, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________

Puisque la valeur p est inférieure à 0,05, nous rejetons l'hypothèse nulle. Nous avons suffisamment de preuves pour affirmer que les données de l'échantillon ne proviennent pas d'une distribution normale. Aucunne variables ne suit la loi normale.

En l'absence de validation de l'hypothèse de normalité, te test de l'ANOVA n'est pas pertinent : Les variables ont une distribution qui change en fonction des valeurs de nutrition_grade_fr

Le nutriscore est-il bien distribué, temoignant du repartition équilibrée des produits ?¶

In [36]:
# On s'occupe ici uniquement des nutrigrades complétés
df_nutri = df[~(df['nutrition_grade_fr']=='0')]
# On s'occupe ici uniquement des nutrigrades complétés
df_nutriscore = df[~(df['nutrition_score_fr_100g']=='0')]
df_nutriscore = df_nutriscore[~(df_nutriscore['nutrition_grade_fr']=='0')] 
In [37]:
# Courbe de distribution du nutriscore
plt.figure(figsize=(12, 8))

sns.histplot(df_nutriscore['nutrition_score_fr_100g'], kde=True,
             color='SteelBlue', label='Nutri_score pour 100g de produit')
plt.title("Distribution du nutri-score", fontsize=14)
plt.xlim(-15, 40)
plt.xlabel('Score', fontsize=12)
plt.ylabel('Nombre de produits par score', fontsize=12)
plt.legend()
plt.savefig("assets/graphiques/Distribution des nutriscore en fonction nb produits.jpg")
plt.show()
  • 2 ensembles sont plus présent : plus de produit qui ont un score entre -1 et 2 et les produits et ceux ayant un score 11
  • 1 ensemble de score est moins nombreux : ceux qui ont un score entre 4 et 6
In [38]:
fig = plt.figure(figsize=(15, 6))

ax1 = fig.add_subplot(1, 2, 1)
box = sns.boxplot(data=df_nutri['nutrition_score_fr_100g'], color='SteelBlue', ax=ax1)
# box.set(ylabel=unite)

plt.grid(False)

ax2 = fig.add_subplot(1, 2, 2)
ax2 = sm.qqplot(df_nutri['nutrition_score_fr_100g'],
             line='r', ax=ax2)
plt.grid(False)

fig.suptitle('Dispersion des nutrition-score-fr_100g', fontweight='bold', size=14)
plt.savefig("assets/graphiques/Dispersion des nutrition_sdcore.jpg")
plt.show()
In [39]:
test_normalite(df_nutri,'nutrition_score_fr_100g')
Test Shapiro-wilk
stat=0.968
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=56321.897, p=0.000
La distribution ne suit pas la loi normale (P<0,05) 
________________________________________________________

Le diagramme et les tests confirment que la variable nutrition_score_fr ne suit pas une distribution normale

In [40]:
col = ['nutrition_score_fr_100g']
tools.stat_descriptives(df_nutri,col)
Out[40]:
Desc nutrition_score_fr_100g
mean 9.136590
median 10.000000
var 81.874771
std 9.048468
skew 0.115543
kurtosis -1.019810
mode 0 0.0
Min -15.000000
Max 40.000000
In [41]:
# définition des bacs
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.IntervalIndex.from_tuples.html
liste_bins = pd.IntervalIndex.from_tuples(
    [(-15, -1), (0, 2), (3, 10), (10, 18), (19, 40)])
tools.distribution_variables_plages_perc_donnees(df_nutri,'nutrition_score_fr_100g',liste_bins)
Out[41]:
Plage nb_données %_données
(-15, -1] 35328 16.215100
(0, 2] 21567 9.898977
(3, 10] 37771 17.336405
(10, 18] 62507 28.689913
(19, 40] 34358 15.769882

Bilan nutriscore


  • La distribution semble non normale surtout pour les produits de tête et de fin.
  • La moyenne et la médiane sont autour d'un score de 10.
  • L'amplitude est de -15 à 40 donc tous les nutri-scores sont représentés.
  • La majorité des produits d'Open Food Facts nutri-score > 10 (nutrigrade hors boisson : de jaune à orange foncé)

Variables qualitatives ordinales¶

In [42]:
# Variables qualitatives ordinales
cols_qual_ord = ['nutriscore_grade_fr']

Comment exploiter les nutrigrades pour notre application ?¶

In [43]:
# On s'occupe ici uniquement des nutrigrades complétés
df_nutri = df[~(df['nutrition_grade_fr']=='0')]
In [44]:
nutrition_grade = df_nutri.groupby(by='nutrition_grade_fr')['code'].nunique().sort_values(ascending=False)

fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(aspect="equal"))
explodes = np.zeros(5)
explodes[0] = .1

plt.pie(nutrition_grade, labels=nutrition_grade.index, 
        startangle=0, 
        colors=['#ee8100','#fecb02','#e63e11','#038141','#85bb2f'],
        shadow=True,
        explode=explodes,
        autopct='%1.1f%%',
        textprops=dict(color="black",size=12, weight="bold"))
plt.title("Répartition des Nutrition_grade", fontdict=font_title)
plt.savefig("assets/graphiques/Répartion_nutrigrdes.jpg")
plt.show()

Tous les nutrigrades sont représentés avec beaucoup de produits appartenant aus groupes 'd'et 'e' représentant 47% des produits de la base

Définition : que signifie le Nutri-Score ?

Conçu dans le cadre du Programme National Nutrition Santé, le Nutri-Score est une échelle graphique qui classe de A à E les produits alimentaires en fonction de leurs qualités nutritionnelles. Le système retenu se base ainsi sur un code à 5 couleurs : du vert pour les produits équilibrés, du rouge pour les aliments trop gras ou trop sucrés et trois couleurs intermédiaires (vert clair, jaune et orange).

► Les aliments classés A sont les plus favorables sur le plan nutritionnel car il s'agit de nutriments et d'aliments à favoriser (fibres, protéines, fruits, légumes, légumineuses, fruits à coques, huile de colza, de noix et d'olive),

► Les aliments classés E ont une moins bonne qualité nutritionnelle car ils contiennent des nutriments à limiter (énergie, acides gras saturés, sucres, sel).

Il s'agit de l'étiquetage nutritionnel officiel recommandé en France. Mis au point par des équipes de recherches internationales, synthétique, compréhensible et fondé sur des bases scientifiques, ce logo fournit une information immédiate au consommateur sur la qualité nutritionnelle des produits qu'il achète afin de l'aider à faire facilement les bons choix dans les rayons des supermarchés. 

ANALYSE BIVARIEES¶

Les corrélations¶

In [45]:
sns.clustermap(df_nutri.corr(),annot=True)
plt.savefig("assets/graphiques/table de corrélations.jpg")
In [46]:
plt.figure(figsize=(8,8))
sns.set(font_scale=1.5)
plt.title('Matrice de corrélation de pearson entre les différentes features')

corr = df_nutriscore[cols_num].corr()

mask = np.zeros_like(corr, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True 

ax = sns.heatmap(corr, mask=mask, vmin=-1, cmap='coolwarm')
plt.show()
sns.set(font_scale=1)
  • On observe des correlations entre le nutrigrade et les autres variables interressantes pour l'application
  • additives_n : pas de correlation remarquable
  • energy_100g : forte corrélation avec:
    • fat_100g
    • saturated-fat_100g
    • carbohydrates_100g
    • nutrition-score-fr_100g
  • fat_100g et saturated-fat_100g fortement corrélés
  • nutrition-score-fr_100g : forte corrélation avec:
    • saturated_fat_100g
    • energy_100g
    • fat_100g
  • sugars_100g : forte correlation avec carbohydrates_100g
  • sodium_100g correlation très forte avec salt_100g
In [47]:
df_test = df_nutriscore.copy()
# on ajoute une colonne corespondant à la labelEncoder de la variable nutrigrade
le = preprocessing.LabelEncoder()
le.fit(["a", "b", "c", "d","e"])
df_test['nutrition_grade_fr_le'] = le.transform(df_test['nutrition_grade_fr'])

Le test de Shapiro-Wilk est un test de normalité sur de petits échantillons. Il est utilisé pour déterminer si un échantillon provient ou non d'une distribution normale.

Hypothèses :

  • H0 : la distribution des données est normale (P>0,05)
  • H1 : la distribution des données n'est pas normale (P<0,05)

Comme la valeur p est inférieure à 0,05, nous rejetons l'hypothèse nulle. les données de l'échantillon ne proviennent pas d'une distribution normale.


normaltest basé sur D'Agostino an Pearson's

Hypothèses :

  • H0 : la distribution des données est normale (P>0,05)
  • H1 : la distribution des données n'est pas normale (P<0,05)

Création d'un fonction pour faire les tests

In [48]:
def test_normalite(df,var):
    ''' Création d'une fonction pour tester la normalioté de la distribution des variables
    '''
    # print(f"Variable : {var}")
    stat, p = shapiro(df[var]) 
    print ('Test Shapiro')
    print('stat=%.3f\np=%.3f' % (stat,p))
    #interprétation
    if p > 0.05:
        print("Distribution probablement gaussienne")
    else:
        print("Distribution non Gaussienne")
    print("------------------------------------------")
    print("Test normaltest (d'Agostino)")
    stat, p = normaltest(df[var])
    print('Statistics=%.3f, p=%.3f' % (stat, p))
# interpretation
    alpha = 0.05
    if p < alpha:  # null hypothesis: x comes from a normal distribution
        print("H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) ")
    else:
        print("H0 ne peut être rejetée - la distribution des données suit une loi normale (P>0,05)")
    print("________________________________________________________")

Produits | targets¶

  • targets = energie, sel, protéine
In [49]:
def histnutri(col,titre):
    bins = [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95]
   
    plt.figure(figsize=(8,5))

    plt.hist(df_nutri[col], bins=bins, color='#abcdef')

    plt.xticks(bins, fontsize=12)

    plt.ylabel('Nombre de produits')
    plt.xlabel('Quantite de '+ titre)
    plt.title('Quantite de '+ titre + ' dans les produits')

    plt.show()
In [50]:
histnutri('energy_100g','energie pour 100g')
  • Les produits de la base sont plutôt caloriques, ...
In [51]:
histnutri('sodium_100g','Sodium pour 100g')

..., contenant en majorité peu de sodium mais suffisament pour être significatif pour notre application,...

In [52]:
histnutri('proteins_100g','Proteines pour 100g')

... et contenant une quantité de protéine assez bien répartit entre 5 et 50 g

Nutrigrade | variables¶

In [53]:
sns.pairplot(df_nutri.sample(frac=0.05), hue="nutrition_grade_fr")
plt.savefig("assets/graphiques/Pairplot_Nutrition grade.jpg")
In [54]:
# Répartition des variables quantitative en fonction du nutrigrade
colors_nutri = ['#038141','#85bb2f','#fecb02','#ee8100','#e63e11']
fig = plt.figure(figsize=(20, 35))

for i, c in enumerate(df_nutriscore.select_dtypes('float'), 1):
    ax = fig.add_subplot(6, 2, i)
    sns.boxplot(data=df_nutriscore, x='nutrition_grade_fr', y=c,order='abcde', ax=ax,palette=colors_nutri)
    plt.grid(False)
    plt.xticks(fontsize=16)
    plt.yticks(fontsize=16)

plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.suptitle('Répartition des variables quantitatives en fonction du nutrigrade', fontsize=30)
plt.savefig("assets/graphiques/Répartition des variables quantitatives en fonction du nutrigrade.jpg")
plt.show()
  • Répartition générale :

    • Les données sont généralement asymétriques, la majorité d'entre elles sont situées sur le côté supérieur ou inférieur du graphique et présentent de nombreux outliers. L'asymétrie indique que les données peuvent ne pas être normalement distribuées.
      • Beaucoup de valeurs atypiques quelque soit le groupe
        • la diversité des produits explique ces résultats
  • Energie : Plus un produit apporte de l'énegie plus il faut surveiller les quantités (classes CDE)

  • fat, satured-fat : Plus un produit
    • Centres : AB peu gras, CDE : plus gras, à surveiller
    • Dispersion : difference de dispersion CDE
  • Sucres :
    • Centres : Plus un produit contient de sucre plus son nutrigrade augmente
    • Dispersion : beaucoup d'outliers dans les AB mais l'apport de sucre est limité 80g pourt 100g
  • Protéines :
    • Centres (comparer les médianes) : les médianes sont proches entre les groupes
    • Dispersion : Très marquées par les outliers
  • Sel (sodium et sel): Plus un produit contient de sel plus il est à surveiller
    • Centres (comparer les médianes) : Les produits à éviter et à surveiller font parti de ABC
    • Dispersion : beaucoup de produit très salé appartiennent aux groupes considérés comme à maîtriser : C,D,E
  • Nutri-score : conforme à la séparation nutrigraded (assez normal).

  • On regarde les proteines importantes pour notre application :

    • Hypothèse plus un produit est considéré comme sain (A, B) moins il contient de protéines

Nutrigrade | Nutriscore¶

In [55]:
# On s'occupe ici uniquement des nutrigrades et nutriscore complétés
df_nutriscore = df[~(df['nutrition_score_fr_100g']=='0')]
df_nutriscore = df_nutriscore[~(df_nutriscore['nutrition_grade_fr']=='0')] 
In [56]:
# graph
sns.histplot(data=df_nutriscore.sort_values("nutrition_grade_fr"), x="nutrition_score_fr_100g", hue="nutrition_grade_fr")
plt.savefig("assets/graphiques/analyse bivariée_histplot nutriscore.jpg")
plt.show()
In [57]:
fig, axes = plt.subplots(1, 2, sharex=False, sharey=False, figsize=(21,8))
fig.suptitle(r"Répartition des scores Nutriscore et de leurs grades" "\n", fontsize=22)

sns.histplot(data=df_nutriscore.sort_values("nutrition_grade_fr"), x="nutrition_grade_fr", hue="nutrition_grade_fr", ax=axes[0])
axes[0].set_title('Grades de Nutriscores')
axes[0].set_xlabel("nutrition_grade_fr")
axes[0].set_ylabel("Nombre de produits")

sns.histplot(data=df_nutriscore.sort_values("nutrition_grade_fr"), x="nutrition_score_fr_100g", hue="nutrition_grade_fr", ax=axes[1])
axes[1].set_title('Scores de Nutriscores')
axes[1].set_xlabel("Score Nutriscore")
axes[1].set_ylabel("Nombre de produits")
plt.savefig("assets/graphiques/analyse bivariée nutriscore_nutrigrade.jpg")
plt.show()
In [58]:
# Préparation des variables de travail pour les graphiques et les tests
gb = df_nutriscore.groupby('nutrition_grade_fr')['nutrition_score_fr_100g']
df_nutriscore_nutrigrade = pd.DataFrame([gb.get_group(n).values for n in list('abcde')],
                             index=list('abcde')).T
In [59]:
tools.stat_descriptives(df_nutriscore_nutrigrade, ['a', 'b', 'c', 'd', 'e'])
Out[59]:
Desc a b c d e
mean -3.410902 0.906251 6.368761 14.064646 21.949430
median -3.000000 1.000000 6.000000 14.000000 22.000000
var 4.609514 0.747659 6.333577 5.341359 10.357889
std 2.146978 0.864673 2.516660 2.311138 3.218367
skew -0.667080 -0.571789 0.033146 -0.085899 -0.133188
kurtosis 0.750778 3.636181 -1.416087 -0.270737 1.983218
mode 0 -1.0 0 0.0 0 3.0 0 14.0 0 20.0
Min -15.000000 -10.000000 2.000000 6.000000 10.000000
Max 17.000000 2.000000 10.000000 18.000000 40.000000
In [60]:
test_normalite(df_test,'nutrition_score_fr_100g')
Test Shapiro
stat=0.968
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=56321.897, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
  • Lien nutriscore nutrigrade est siginificatif :
    • les moyennes témoignent du classement :
      • plus le nutrition_score es bas plus sa moyenne est basse et inversement
  • nutrigrade :
    • La répartition du nombre de produits en fonction de leur appartenance a un groupe Grade de Nutriscore :
      • Le groupe de produit d est le plus nombreux
        • Les produits considérés comme a surveillés sont bien représenté (d et e)
      • Les 5 groupes sont présents
    • Concernant les scores entre -15 et 40 :
      • Les scores les plus représentés sont en entre 0 et 5
      • Pic autour des 15 (grade d)
      • Les grade sont bien représentatifs des scores et sont plutôt diversifiés

note : Les variances ne sont pas comparables, la distribution des valeurs ne suit pas une loi normale

Nutrigrade | Protéines¶

  • Question :
    • La quantité de protéine est-elle liée au nutrigrade ?
In [61]:
# Préparation des variables de travail pour les graphiques et les tests
gp = df_nutriscore.groupby('nutrition_grade_fr')['proteins_100g']
df_nutrigrade_protein = pd.DataFrame([gp.get_group(n).values for n in list('abcde')],
                             index=list('abcde')).T
df_nutrigrade_protein = df_nutrigrade_protein.dropna()
df_a = df_nutrigrade_protein['a']
df_b = df_nutrigrade_protein['b']
df_c = df_nutrigrade_protein['c']
df_d = df_nutrigrade_protein['d']
df_e = df_nutrigrade_protein['e']
In [62]:
plt.figure(figsize=[10, 10])
# colors_nutri = ['#038141','#85bb2f','#fecb02','#ee8100','#e63e11']
# Boxplot protéines/nutri-score grade
plt.subplot(2, 1, 1)
sns.boxplot(data=df_nutriscore, x='nutrition_grade_fr', y='proteins_100g',
            palette=colors_nutri, order='abcde')
plt.ylim(0, 100)
plt.ylabel('Nombre de g de protéines pour 100g de produit', fontsize=12)
plt.xlabel('Nutri-grade_fr', fontsize=12)
plt.title('Protéines par nutri-grade', fontsize=14)
plt.grid(False)
# Ajout moyenne des protéines pour tous les produits
moyenne_proteines = df_nutriscore['proteins_100g'].mean()
plt.axhline(y=moyenne_proteines, color='r')

# Violinplot protéines/nutri-score grade
plt.subplot(2, 1, 2)
sns.violinplot(data=df_nutriscore, x='nutrition_grade_fr', y='proteins_100g',
               palette=colors_nutri, order='abcde')
plt.ylabel('Nombre de g de protéines pour 100g de produit', fontsize=12)
plt.xlabel('Nutri-grade', fontsize=12)
plt.grid(False)
# Ajout moyenne des protéines pour tous les produits
plt.axhline(y=moyenne_proteines, color='r')
plt.savefig("assets/graphiques/analyse bivariée Protein_nutrigrade.jpg")
plt.show()
  • amplitude en 0 et 100g de protéines
  • pas de split significatif entre nutrigrade et nombre de protéine pour 100g
In [63]:
# Distplot protéines
sns.distplot(df_nutriscore['proteins_100g'], bins=100, color='SteelBlue')
plt.savefig("assets/graphiques/analyse univarié répartition protéines.jpg")
plt.grid(False)
In [64]:
qqplot(df_nutriscore['proteins_100g'], line='r')
plt.grid(False)
plt.savefig("assets/graphiques/analyse univariée proteines qqplot.jpg")
plt.show()
In [65]:
test_normalite(df_nutriscore,'proteins_100g')
Test Shapiro
stat=0.826
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=96260.154, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
  • amplitude en 0 et 100g de protéines
  • pas de correlation entre nutrigrade et nombre de protéine pour 100g
  • La distribution ne semble pas suivre une distribution normal
In [66]:
# Statistiques descriptives
tools.stat_descriptives(df_nutriscore, ['proteins_100g'])
Out[66]:
Desc proteins_100g
mean 7.784885
median 5.700000
var 65.210136
std 8.075279
skew 2.000562
kurtosis 7.605163
mode 0 0.0
Min 0.000000
Max 100.000000

On visualise la distribution des protéines

In [67]:
import matplotlib.patches as mpatches

fig = plt.figure(figsize=(8, 6))
label_patches = []

sns.kdeplot(df_nutriscore['proteins_100g'], color='Blue')
label_patch = mpatches.Patch(
    color='Blue',
    label='Ensemble des nutrigrades')
label_patches.append(label_patch)
plt.grid(False)
plt.xlim([-1, 40])

i = 1
for n, c in zip(list('abcde'), colors_nutri):
    i += 1
    sns.kdeplot(df_nutrigrade_protein[n], color=c)
    label_patch = mpatches.Patch(color=c, label=n)
    label_patches.append(label_patch)
    plt.grid(False)
    plt.xlim([-1, 40])

fig.suptitle('Distribution des protéines', fontweight='bold', fontsize=14)
plt.legend(handles=label_patches,bbox_to_anchor=(1.05,1),loc=2,borderaxespad=0., facecolor='white')
plt.tight_layout()
plt.grid(False)
plt.savefig("assets/graphiques/analyse bivzartiée nutrigrade_protein_ distribution.jpg")
plt.show()
  • L'essentiel des produits entre 0 et 15g de sodium
    • Tous les nutrigrades s'étalent entre 0 et 30
    • beaucoup de produit de la classe b entre 0 et 15 mais a en 0 et 15 ==> à approfondir On visualise la distribution des protéines par nutrigrade pour voir si des tendances peuvent exister
In [68]:
# Histogramme des protéines, général et par nutri-score
def graph_distribution(df,df_nutrigrade,var):
    fig = plt.figure(figsize=(8, 15))

    fig.add_subplot(6, 1, 1)
    sns.distplot(df[var], bins=70, color='SteelBlue')
    plt.grid(False)

    i = 1
    for n, c in zip(list('abcde'), colors_nutri):
        i += 1
        ax = fig.add_subplot(6, 1, i)
        sns.distplot(df_nutrigrade[n], color=c, bins=70)
        plt.grid(False)
    fig.suptitle(f'Histogramme des {var}',
                 fontweight='bold', fontsize=14)

    plt.tight_layout(rect=[0, 0.0, 1, 0.93])
    plt.grid(False)
    plt.show()
In [69]:
graph_distribution(df_nutriscore,df_nutrigrade_protein,'proteins_100g')
plt.savefig("assets/graphiques/analyse bivariée proteines par nutrigrade.jpg")
<Figure size 640x480 with 0 Axes>
  • Aucun nutrigrade ne se distingue au regard de la quantité de proteins pour 100 g ni dans leurs densités
In [70]:
# On test la normalité de la distribution sur les proteines par nutrigrade
for col in df_nutrigrade_protein.columns:
    print(f"Nutrigrade : {col}")
    test_normalite(df_nutrigrade_protein,col)
Nutrigrade : a
Test Shapiro
stat=0.855
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=17448.344, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
Nutrigrade : b
Test Shapiro
stat=0.726
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=24786.383, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
Nutrigrade : c
Test Shapiro
stat=0.827
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=15439.283, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
Nutrigrade : d
Test Shapiro
stat=0.842
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=11675.052, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
Nutrigrade : e
Test Shapiro
stat=0.828
p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=9922.017, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
In [71]:
liste_bins = pd.IntervalIndex.from_tuples(
    [(0, 4), (5, 9), (10, 14), (15, 19), (20, 24), (25, 29),
     (30, 34), (35, 39), (40, 100)])
tools.distribution_variables_plages_perc_donnees(df_nutriscore, 'proteins_100g', liste_bins)
Out[71]:
Plage nb_données %_données
(0, 4] 62085 28.496220
(5, 9] 47832 21.954276
(10, 14] 21110 9.689220
(15, 19] 10388 4.767959
(20, 24] 8823 4.049644
(25, 29] 3188 1.463251
(30, 34] 1084 0.497542
(35, 39] 448 0.205626
(40, 100] 939 0.430989
In [72]:
df_nutriscore['proteins_100g'].describe()
Out[72]:
count    217871.000000
mean          7.784885
std           8.075298
min           0.000000
25%           1.900000
50%           5.700000
75%          10.710000
max         100.000000
Name: proteins_100g, dtype: float64
In [73]:
df_nutrigrade_protein.describe()
Out[73]:
a b c d e
count 33771.000000 33771.000000 33771.000000 33771.000000 33771.000000
mean 8.355183 5.086969 7.160575 8.321832 9.566366
std 7.064520 6.588239 7.577890 8.558382 9.389601
min 0.000000 0.000000 0.000000 0.000000 0.000000
25% 3.000000 0.600000 1.200000 2.630000 3.450000
50% 7.140000 3.100000 5.320000 6.060000 6.200000
75% 12.280000 7.300000 10.530000 12.500000 13.790000
max 100.000000 100.000000 93.330000 80.000000 100.000000
In [74]:
#  Test de Bartlett's
# H0 : les groupes sont homoscédastiques,variances identiques
# p_value>0.05 rejet de H1 en faveur de H0
# p_value<=0.05 H0 rejetée en faveur de H1
# H1 : les groupes sont hétéroscédastiques variances différentes

F_val, p_value = st.bartlett(df_a, df_b, df_c, df_d,df_e)
print(f'Test Bartlett - resultats: F={F_val}, P_value ={p_value}\n')
print('Groupes probablement homoscédastiques') if p_value > 0.05 else print(
    'Groupes probablement hétéroscédastiques')
Test Bartlett - resultats: F=5603.926752538663, P_value =0.0

Groupes probablement hétéroscédastiques

Conclusion : p_value<0.05 on rejette H0 au profit de H1, les groupes sont hétéroscédastiques. Tout au moins on ne peut pas dire que les variances sont significativement identiques. Ne remplit pas les conditions pour l'anova (distribution normal, homocedasticité,indépendance)

In [75]:
# ANOVA pour la forme mais pas pertinent
X = 'nutrition_grade_fr'  # qualitative
Y = 'proteins_100g'  # quantitative

def eta_squared(x, y):
    moyenne_y = y.mean()
    nutris = []
    for nutri in x.unique():
        yi_nutri = y[x == nutri]
        nutris.append({'ni': len(yi_nutri),
                       'moyenne_nutri': yi_nutri.mean()})
    SCT = sum([(yj-moyenne_y)**2 for yj in y])
    SCE = sum([c['ni']*(c['moyenne_nutri']-moyenne_y)**2 for c in nutris])
    return SCE/SCT


eta_squared(df_nutriscore[X], df_nutriscore[Y])
Out[75]:
0.02867203470252973

Faible relation entre la variable protéine et le nutrigrade.

In [76]:
# Anova OLS
anova_nutrigrade = smf.ols('proteins_100g~nutrition_grade_fr', data=df_nutriscore).fit()
print(anova_nutrigrade.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:          proteins_100g   R-squared:                       0.029
Model:                            OLS   Adj. R-squared:                  0.029
Method:                 Least Squares   F-statistic:                     1608.
Date:                Sat, 21 Jan 2023   Prob (F-statistic):               0.00
Time:                        16:48:25   Log-Likelihood:            -7.6107e+05
No. Observations:              217871   AIC:                         1.522e+06
Df Residuals:                  217866   BIC:                         1.522e+06
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                   8.3227      0.042    196.360      0.000       8.240       8.406
nutrition_grade_fr[T.b]    -3.2357      0.061    -53.397      0.000      -3.355      -3.117
nutrition_grade_fr[T.c]    -1.3568      0.057    -23.953      0.000      -1.468      -1.246
nutrition_grade_fr[T.d]     0.2917      0.053      5.493      0.000       0.188       0.396
nutrition_grade_fr[T.e]     0.8290      0.057     14.431      0.000       0.716       0.942
==============================================================================
Omnibus:                    97193.410   Durbin-Watson:                   0.894
Prob(Omnibus):                  0.000   Jarque-Bera (JB):           721057.540
Skew:                           2.000   Prob(JB):                         0.00
Kurtosis:                      10.964   Cond. No.                         6.46
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
  • amplitude en 0 et 100g de protéines
  • pas de correlations flagrantes entre nutrigrade et nombre de protéine pour 100g
  • On rertrouve dans toutes les plages toutes les notes du nutrtion grade
  • 75% des variables contiennent 11g de proteines

  • R² et R² ajusted : SCE/SCT, sont inférieurs à 5% : 0.028 : la variable explicative nutrition_grade_fr n'est pas pertinentes pour expliquer proteins_100g
  • Omnibus : Une valeur proche de zero indique que les données suivent une loi Normal
    • Prob(Obminbus) : Confirme que les données ne suivent pas une loi normale (proche de 1) mais dans notre ici 0.000
  • Caractéristiques de forme
    • Coefficient d'applatissement Kurtosis : 11.242

  • CCL : Rappel de la question : La quantité de protéine est-elle liée au nutrigrade ?
    • Elles est liée mais la quantité de protéine est un des facteurs pris en considération, on ne peut pas distinguer les nutritions grade en fonction de la variable proteins

Nutrigrade | Sodium¶

Question :
    La quantité de sodium est-elle liée au nutrigrade ?
In [77]:
# Préparation des variables de travail pour les graphiques et les tests
gb = df_nutriscore.groupby('nutrition_grade_fr')['sodium_100g']
df_nutrigrade_sodium = pd.DataFrame([gb.get_group(n).values for n in list('abcde')],index=list('abcde')).T
In [78]:
import matplotlib.patches as mpatches

fig = plt.figure(figsize=(8, 6))
label_patches = []

sns.kdeplot(df_nutriscore['sodium_100g'], color='Blue')
label_patch = mpatches.Patch(
    color='Blue',
    label='Ensemble des nutrigrades')
label_patches.append(label_patch)
plt.grid(False)
plt.xlim([-1, 40])

i = 1
for n, c in zip(list('abcde'), colors_nutri):
    i += 1
    sns.kdeplot(df_nutrigrade_sodium[n], color=c)
    label_patch = mpatches.Patch(color=c, label=n)
    label_patches.append(label_patch)
    plt.grid(False)
    plt.xlim([-1, 40])

fig.suptitle('Distribution sodium', fontweight='bold', fontsize=14)
plt.legend(handles=label_patches,bbox_to_anchor=(1.05,1),loc=2,borderaxespad=0., facecolor='white')
plt.tight_layout()
plt.grid(False)
plt.savefig("assets/graphiques/analyse bivariée sodium nutrigrade .jpg")
plt.show()
In [79]:
df_nutriscore['sodium_100g'].describe()
Out[79]:
count    217871.000000
mean          0.487246
std           1.552487
min           0.000000
25%           0.039370
50%           0.255906
75%           0.536000
max          39.370079
Name: sodium_100g, dtype: float64
In [80]:
liste_bins = pd.IntervalIndex.from_tuples(
    [(0, 0.2), (0.3, 0.5), (0.5, 0.7), (0.8, 1), (1.1, 1.5), (1.6, 2.5),
     (2.6, 3), (3, 4), (4, 40)])
tools.distribution_variables_plages_perc_donnees(df_nutriscore, 'sodium_100g', liste_bins)
Out[80]:
Plage nb_données %_données
(0.0, 0.2] 76347 35.042296
(0.3, 0.5] 40246 18.472399
(0.5, 0.7] 23808 10.927567
(0.8, 1.0] 9134 4.192389
(1.1, 1.5] 6467 2.968270
(1.6, 2.5] 4232 1.942434
(2.6, 3.0] 454 0.208380
(3.0, 4.0] 644 0.295588
(4.0, 40.0] 2356 1.081374
  • La majorité des produit sont entre 0 et 0.2 g de sodium
  • 75% des données ont des valeur < 0.5
In [81]:
fig = plt.figure(figsize=(8, 15))

fig.add_subplot(6, 1, 1)
sns.distplot(df_nutriscore['sodium_100g'], bins=70, color='SteelBlue')
plt.grid(False)

i = 1
for n, c in zip(list('abcde'), colors_nutri):
    i += 1
    ax = fig.add_subplot(6, 1, i)
    sns.distplot(df_nutrigrade_sodium[n], color=c, bins=70)
    plt.grid(False)
fig.suptitle(f'Histogramme des {var}',
             fontweight='bold', fontsize=14)

plt.tight_layout(rect=[0, 0.0, 1, 0.93])
plt.grid(False)



plt.savefig("assets/graphiques/analyse bivariée sodium par nutrigrade.jpg")
plt.show()
In [82]:
df_nutrigrade_sodium.describe()
Out[82]:
a b c d e
count 35259.000000 33771.000000 44861.000000 61860.000000 42120.000000
mean 0.130397 0.205712 0.673686 0.640764 0.587656
std 0.154637 0.274928 2.672363 1.467174 1.169250
min 0.000000 0.000000 0.000000 0.000000 0.000000
25% 0.007000 0.016000 0.050000 0.071000 0.098856
50% 0.051181 0.167323 0.346457 0.365000 0.357000
75% 0.240157 0.362205 0.576000 0.718000 0.728346
max 4.666600 30.708661 39.370079 35.710000 39.370079
  • Plus un produit contient du sel plus son score est elevé et moins il est considéré comme sain
In [83]:
# Anova OLS
anova_nutrigrade = smf.ols('sodium_100g~nutrition_grade_fr', data=df_nutriscore).fit()
print(anova_nutrigrade.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:            sodium_100g   R-squared:                       0.020
Model:                            OLS   Adj. R-squared:                  0.020
Method:                 Least Squares   F-statistic:                     1123.
Date:                Sat, 21 Jan 2023   Prob (F-statistic):               0.00
Time:                        16:48:38   Log-Likelihood:            -4.0275e+05
No. Observations:              217871   AIC:                         8.055e+05
Df Residuals:                  217866   BIC:                         8.056e+05
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                   0.1304      0.008     15.933      0.000       0.114       0.146
nutrition_grade_fr[T.b]     0.0753      0.012      6.437      0.000       0.052       0.098
nutrition_grade_fr[T.c]     0.5433      0.011     49.674      0.000       0.522       0.565
nutrition_grade_fr[T.d]     0.5104      0.010     49.770      0.000       0.490       0.530
nutrition_grade_fr[T.e]     0.4573      0.011     41.222      0.000       0.436       0.479
==============================================================================
Omnibus:                   420773.343   Durbin-Watson:                   1.222
Prob(Omnibus):                  0.000   Jarque-Bera (JB):        834700958.729
Skew:                          15.400   Prob(JB):                         0.00
Kurtosis:                     304.661   Cond. No.                         6.46
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
  • R² et R² ajusted : SCE/SCT, sont inférieurs à 5% : 0.020 : la variable explicative nutrition_grade_fr n'est pas pertinentes pour expliquer proteins_100g
  • Omnibus : Une valeur proche de zero indique que les données suivent une loi Normal
    • Prob(Obminbus) : Confirme que les données ne suivent pas une loi normale (proche de 1) mais dans notre ici 0.000
  • Caractéristiques de forme
    • Coefficient d'applatissement Kurtosis : 15.2

ANALYSE MULTIVARIEE¶

  • Toutes les variables sont nécéssaires à par le sel d'après cette algorithme.

  • On regarde maintenant qu'elle est l'importance de l'énérgie et du nutrition_score

In [84]:
# On cherche l'élément qui explique le mieux le nutrigrade
anova_nutrigrade = smf.ols('energy_100g~nutrition_score_fr_100g', data=df_test).fit()
print(anova_nutrigrade.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:            energy_100g   R-squared:                       0.403
Model:                            OLS   Adj. R-squared:                  0.403
Method:                 Least Squares   F-statistic:                 1.469e+05
Date:                Sat, 21 Jan 2023   Prob (F-statistic):               0.00
Time:                        16:48:38   Log-Likelihood:            -1.6977e+06
No. Observations:              217871   AIC:                         3.395e+06
Df Residuals:                  217869   BIC:                         3.395e+06
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                 688.1177      1.784    385.658      0.000     684.621     691.615
nutrition_score_fr_100g    53.1863      0.139    383.305      0.000      52.914      53.458
==============================================================================
Omnibus:                    15883.565   Durbin-Watson:                   0.859
Prob(Omnibus):                  0.000   Jarque-Bera (JB):            19681.842
Skew:                           0.701   Prob(JB):                         0.00
Kurtosis:                       3.451   Cond. No.                         18.3
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
In [85]:
# On cherche l'élément qui explique le mieux le nutrigrade
anova_nutrigrade = smf.ols('nutrition_grade_fr_le~nutrition_score_fr_100g', data=df_test).fit()
print(anova_nutrigrade.summary())
                              OLS Regression Results                             
=================================================================================
Dep. Variable:     nutrition_grade_fr_le   R-squared:                       0.916
Model:                               OLS   Adj. R-squared:                  0.916
Method:                    Least Squares   F-statistic:                 2.368e+06
Date:                   Sat, 21 Jan 2023   Prob (F-statistic):               0.00
Time:                           16:48:38   Log-Likelihood:            -1.0505e+05
No. Observations:                 217871   AIC:                         2.101e+05
Df Residuals:                     217869   BIC:                         2.101e+05
Df Model:                              1                                         
Covariance Type:               nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                   0.8874      0.001    743.715      0.000       0.885       0.890
nutrition_score_fr_100g     0.1428   9.28e-05   1538.812      0.000       0.143       0.143
==============================================================================
Omnibus:                     2294.738   Durbin-Watson:                   1.457
Prob(Omnibus):                  0.000   Jarque-Bera (JB):             3868.063
Skew:                           0.048   Prob(JB):                         0.00
Kurtosis:                       3.646   Cond. No.                         18.3
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
  • C'est essentiellement le nutrtion score qui explique l'appartenance au nutrigrade qui se calcule avec les variables nutritives et l'energy_100g est trés important d'après le R²

  • On regarde maintenant via l'ACP quelles sont les variables les mieux représentées et quelles sont leur relations

Analyse en Composantes Principales (ACP)¶

In [86]:
df_acp = df_nutriscore.copy()
In [87]:
df_acp.columns
Out[87]:
Index(['code', 'creator', 'created_datetime', 'last_modified_datetime',
       'product_name', 'brands', 'categories_fr', 'countries_fr',
       'additives_n', 'additives_fr', 'ingredients_from_palm_oil_n',
       'nutrition_grade_fr', 'main_category_fr', 'energy_100g', 'fat_100g',
       'saturated_fat_100g', 'carbohydrates_100g', 'sugars_100g', 'fiber_100g',
       'proteins_100g', 'salt_100g', 'sodium_100g', 'nutrition_score_fr_100g'],
      dtype='object')
In [88]:
# On isole les variables de notre ACP
cols_acp = ['energy_100g','fat_100g', 'saturated_fat_100g', 'carbohydrates_100g', 'sugars_100g',
           'fiber_100g', 'proteins_100g', 'salt_100g', 'sodium_100g','nutrition_score_fr_100g']
In [89]:
#Centrage et réduction
X = df_acp[cols_acp]
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

#Instanciation de l'ACP
pca = PCA(svd_solver='full').fit(X_scaled)
X_projected = pca.transform(X_scaled)

Eboulis des valeurs propres Afin d'avoir un aperçu du nombre de composantes nécessaire à l'analyse, nous allons projeter l'éboulis des valeurs propres :

In [90]:
#Variances expliquées
varexpl = pca.explained_variance_ratio_*100

#Projection de l'éboulis des valeurs propres
taux_var_exp = pca.explained_variance_ratio_
scree = taux_var_exp * 100
plt.bar(np.arange(len(scree)) + 1, scree, color='SteelBlue')
ax1 = plt.gca()
ax2 = ax1.twinx()
ax2.plot(np.arange(len(scree)) + 1, scree.cumsum(), c='red', marker='o')
ax2.set_ylabel('Taux cumulatif de l\'inertie')
ax1.set_xlabel('Rang de l\'axe d\'inertie')
ax1.set_ylabel('Pourcentage d\'inertie')
for i, p in enumerate(ax1.patches):
    ax1.text(p.get_width() /
             5 +
             p.get_x(),
             p.get_height() +
             p.get_y() +
             0.3,
             '{:.0f}%'.format(taux_var_exp[i] *100),
             fontsize=8,color='k')
plt.title("Eboulis des valeurs propres", fontdict=font_title)
plt.gcf().set_size_inches(8, 4)
plt.grid(False)
plt.show(block=False)
In [91]:
print("Le premier plan factoriel couvre une inertie de {:.2f}% et le second plan : {:.2f}%.".format(varexpl[0:2].sum(),
                                                                                                     varexpl[0:4].sum()))
Le premier plan factoriel couvre une inertie de 52.57% et le second plan : 83.27%.

Les 2 premiers plans factoriels couvrent une inertie d'un peu plus de 83,15%. Une analyse sur F1 et F2 semble donc cohérente.

Projection sur le cercle des corrélations

In [92]:
#Espace des composantes principales
pcs = pca.components_

#Matrice des corrélations variables x facteurs
p = X.shape[1]
sqrt_valprop = np.sqrt(pca.explained_variance_)
corvar = np.zeros((p, p))
for dim in range(p):
    corvar[:,dim] = pcs[dim,:] * sqrt_valprop[dim]

#on affiche pour les deux premiers plans factoriels 
corr_matrix = pd.DataFrame({'feature':X.columns,'CORR_F1':corvar[:,0],'CORR_F2':corvar[:,1], 
              'CORR_F3':corvar[:,2], 'CORR_F4':corvar[:,3]})
corr_matrix
Out[92]:
feature CORR_F1 CORR_F2 CORR_F3 CORR_F4
0 energy_100g 0.920311 -0.058511 0.006912 0.208189
1 fat_100g 0.778572 0.151401 -0.413616 -0.049046
2 saturated_fat_100g 0.753646 0.109080 -0.316516 -0.284172
3 carbohydrates_100g 0.408202 -0.347636 0.670014 0.341026
4 sugars_100g 0.432569 -0.378023 0.679152 -0.147291
5 fiber_100g 0.187394 -0.077619 -0.016578 0.861174
6 proteins_100g 0.258108 0.267153 -0.499100 0.440871
7 salt_100g 0.005161 0.921201 0.381786 0.055700
8 sodium_100g 0.005156 0.921203 0.381781 0.055704
9 nutrition_score_fr_100g 0.830952 0.114662 0.138296 -0.349583
In [93]:
#Variable Illustrative
ivNutrigrade = df_acp['nutrition_grade_fr'].values

#Encodage des grades pour l'acp
from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
ivNutrigrade = encoder.fit_transform(ivNutrigrade)
ivNutrigrade = ivNutrigrade.reshape((ivNutrigrade.shape[0],1))

#Corrélation de la variable illustrative avec les axes factoriels 
corrIv = np.zeros((ivNutrigrade.shape[1],p))
for j in range(p): 
    for k in range(ivNutrigrade.shape[1]): 
        corrIv[k,j] = np.corrcoef(ivNutrigrade[:,k],X_projected[:,j])[0,1]
In [94]:
def cerle_corr(pcs, n_comp, pca, axis_ranks, 
               labels=None, label_rotation=0, 
               illustrative_var_label=None, illustrative_var_corr=None):
    for d1, d2 in axis_ranks:
        if d2 < n_comp:
            
            # initialisation de la figure
            fig=plt.figure(figsize=(10,10))
            fig.subplots_adjust(left=0.1,right=0.9,bottom=0.1,top=0.9)
            ax=fig.add_subplot(111)
            ax.set_aspect('equal', adjustable='box') 
            ax.set_facecolor("white")

            #détermination des limites du graphique
            ax.set_xlim(-1,1) 
            ax.set_ylim(-1,1) 

            #affichage des flèches 
            plt.quiver(np.zeros(pcs.shape[1]), np.zeros(pcs.shape[1]),
                       pcs[d1,:],pcs[d2,:], 
                       angles='xy', scale_units='xy', scale=1, 
                       color="black", alpha=0.5)
            # et noms de variables
            for i,(x,y) in enumerate(pcs[[d1,d2]].T):
                plt.annotate(labels[i],(x,y),
                             ha='center', va='center',
                             fontsize='14',color="black", alpha=0.8) 

            #variable illustrative
            if illustrative_var_label is not None :
                plt.annotate(illustrative_var_label,
                             (illustrative_var_corr[0,d1],illustrative_var_corr[0,d2]),
                             color='b')
                plt.quiver(np.zeros(pcs.shape[1]), np.zeros(pcs.shape[1]),
                                   illustrative_var_corr[0,d1],illustrative_var_corr[0,d2], 
                                   angles='xy', scale_units='xy', scale=1, color="b", alpha=0.5)

            #ajouter les axes 
            plt.plot([-1,1],[0,0],linewidth=1, color='black', ls='--') 
            plt.plot([0,0],[-1,1],linewidth=1, color='black', ls='--')

            #ajouter un cercle 
            cercle = plt.Circle((0,0),1,color='steelblue',fill=False) 
            ax.add_artist(cercle) 

            # nom des axes, avec le pourcentage d'inertie expliqué
            plt.xlabel('F{} ({}%)'.format(d1+1, round(100*pca.explained_variance_ratio_[d1],1)))
            plt.ylabel('F{} ({}%)'.format(d2+1, round(100*pca.explained_variance_ratio_[d2],1)))

            plt.title("Cercle des corrélations (F{} et F{})".format(d1+1, d2+1), fontdict=font_title)
            plt.show(block=False)
In [95]:
cerle_corr(pcs, 4, pca, [(0,1),(2,3)], labels = np.array(X.columns), 
           illustrative_var_label="Nutriscore_grade", illustrative_var_corr = corrIv)

Bilan


F1,F2

  • parties :
    • haut : Produits salés
    • bas : Produit plutôt sucré
    • gauche : peu calorique
    • droite : produit à forte teneur en énergie
  • Par raport au Nutrigrade
    • très corrélés aux produits gras, sucré et au nutrition_score_fr
  • Observations
    • Nutrition_score : très corrélés à : fat, , saturated_fat et nutriscore
    • carbohydrate et sugar
    • sodium et salt

F3, F4

  • Par raport au Nutrigrade
    • Très corrélé au nutrition_score_fr (logique) est dans le bas du graphique (produits putôt gras, sucré et energie
    • Protéine est anticorrelé à l'axe
  • Observations
    • Les produits carbohydrate son anticorele au fat, saturated_fat
    • nutriscore et sugar au protéine (F4)
    • sodium, salt au fat
    • proteins au nutriscore

Projection des produits sur les plans factoriels

  • Visualiser la projection des individus sur ces premiers plans factoriels et donc en 2D :
In [96]:
def plot_plans_factoriels_nutrigrade(X_projected, n_comp, pca, axis_ranks, labels=None, alpha=1, illustrative_var=None):
    for d1,d2 in axis_ranks:
        if d2 < n_comp:
            # initialisation de la figure       
            fig = plt.figure(figsize=(12,8))
        
            # affichage des points
            if illustrative_var is None:
                plt.scatter(X_projected[:, d1], X_projected[:, d2], alpha=alpha)
            else:
                illustrative_var = np.array(illustrative_var)
                label_patches = []
                colors = ['#038141', '#85bb2f', '#fecb02', '#ee8100', '#e63e11']
                i = 0
                for value in np.unique(illustrative_var):
                    selected = np.where(illustrative_var == value)
                    plt.scatter(X_projected[selected, d1], X_projected[selected, d2], alpha=alpha, label=value, c=colors[i])
                    label_patch = mpatches.Patch(color=colors[i],
                                                 label=value)
                    label_patches.append(label_patch)
                    i += 1
                    plt.legend(
                        handles=label_patches,
                        bbox_to_anchor=(1.05,1),loc=2,borderaxespad=0.,facecolor='white')
               # plt.legend()
                

            # affichage des labels des points
            if labels is not None:
                for i,(x,y) in enumerate(X_projected[:,[d1,d2]]):
                    plt.text(x, y, labels[i],
                              fontsize='14', ha='center',va='center') 
                    
            
            # détermination des limites du graphique
            boundary = np.max(np.abs(X_projected[:, [d1,d2]]))*1.1
            plt.xlim([-boundary,boundary])
            plt.ylim([-boundary,boundary])
        
            # affichage des lignes horizontales et verticales
            plt.plot([-100, 100], [0, 0], color='grey', ls='--')
            plt.plot([0, 0], [-100, 100], color='grey', ls='--')

            # nom des axes, avec le pourcentage d'inertie expliqué
            plt.xlabel('F{} ({}%)'.format(d1+1, round(100*pca.explained_variance_ratio_[d1],1)))
            plt.ylabel('F{} ({}%)'.format(d2+1, round(100*pca.explained_variance_ratio_[d2],1)))

            plt.title("Projection des {} individus sur F{} et F{}".format(X_projected.shape[0], d1+1, d2+1), fontdict=font_title)
            plt.show(block=False)
In [97]:
plot_plans_factoriels_nutrigrade(X_projected, 4, pca, [(0,1),(2,3)], illustrative_var = df_nutriscore['nutrition_grade_fr'])

Clustering¶

On tente le clustering : des clusters ptrotein, sodium et sel ?

In [98]:
# Copy et préparation des données
df_cluster_all = df_nutriscore.copy()
df_cluster = df_nutriscore[cols_quant_cont]
In [99]:
# Scaler
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit(df_cluster)
df_cluster_scaled = scaler.transform(df_cluster)
In [100]:
# Méthode du coude pour trouver le nombre de clusters
from sklearn.cluster import KMeans
from yellowbrick.cluster import KElbowVisualizer

# Instantiate 
model = KMeans()
visualizer = KElbowVisualizer(model, k=(1, 11))

# Fit the data to the visualizer
visualizer.fit(df_cluster_scaled)    
plt.grid(False)
visualizer.show()
Out[100]:
<AxesSubplot: title={'center': 'Distortion Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='distortion score'>
In [101]:
# Avec k=3 le meilleur hyper-paramètre pour KMeans
from sklearn.cluster import KMeans

k_means = KMeans(n_clusters=3)
kmeans = k_means.fit(scaler.transform(df_cluster))
df_cluster_all['cluster'] = kmeans.labels_
df_cluster_all.head()
Out[101]:
code creator created_datetime last_modified_datetime product_name brands categories_fr countries_fr additives_n additives_fr ingredients_from_palm_oil_n nutrition_grade_fr main_category_fr energy_100g fat_100g saturated_fat_100g carbohydrates_100g sugars_100g fiber_100g proteins_100g salt_100g sodium_100g nutrition_score_fr_100g cluster
0 0000000004530 usda-ndb-import 2017-03-09 14:32:37 2017-03-09 14:32:37 Banana Chips Sweetened (Whole) inconnue inconnu États-Unis 0.0 0.0 d inconnu 2243.0 28.57 28.57 64.29 14.29 3.6 3.57 0.00000 0.000 14.0 0
1 0000000004559 usda-ndb-import 2017-03-09 14:32:37 2017-03-09 14:32:37 Peanuts Torn & Glasser inconnu États-Unis 0.0 0.0 b inconnu 1941.0 17.86 0.00 60.71 17.86 7.1 17.86 0.63500 0.250 0.0 0
2 0000000016087 usda-ndb-import 2017-03-09 10:35:31 2017-03-09 10:35:31 Organic Salted Nut Mix Grizzlies inconnu États-Unis 0.0 0.0 d inconnu 2540.0 57.14 5.36 17.86 3.57 7.1 17.86 1.22428 0.482 12.0 2
6 0000000016124 usda-ndb-import 2017-03-09 10:35:11 2017-03-09 10:35:12 Organic Muesli Daddy's Muesli inconnu États-Unis 2.0 E123 - Amarante,E307a - Tocophérol 0.0 c inconnu 1833.0 18.75 4.69 57.81 15.62 9.4 14.06 0.13970 0.055 7.0 0
11 0000000016872 usda-ndb-import 2017-03-09 10:34:10 2017-03-09 10:34:11 Zen Party Mix Sunridge inconnu États-Unis 1.0 E100 - Curcumine 0.0 d inconnu 2230.0 36.67 5.00 36.67 3.33 6.7 16.67 1.60782 0.633 12.0 2
In [102]:
plt.figure(figsize=(7, 7))
plt.title("Les Clusters", size=16,weight='bold')
nb_par_var = df_cluster_all['cluster'].sort_values().value_counts()
nb_par_var = nb_par_var.loc[sorted(nb_par_var.index)]
explode = [0.1]
for i in range(len(nb_par_var) - 1):
    explode.append(0)
wedges, texts, autotexts = plt.pie(nb_par_var, labels=nb_par_var.index, autopct='%1.1f%%', 
                                   colors=['b', 'darkblue', 'steelblue'], textprops={
                                       'fontsize': 16, 'color': 'black', 'backgroundcolor': 'w'},
                                   explode=explode)
axes = plt.gca()
axes.legend(wedges,nb_par_var.index,loc='center right',fontsize=14,
            bbox_to_anchor=(1,0,0.5, 1))
plt.savefig("assets/graphiques/clusters.jpg")
plt.show()

On a 3 cluster

In [103]:
plt.figure(figsize=(15, 8))
sns.countplot(x='cluster', hue='nutrition_grade_fr', data=df_cluster_all,
              palette=colors_nutri)

plt.legend(loc=1)
plt.ylabel('Nombre de produits', labelpad=20, fontsize=14)
plt.xticks(fontsize=14)
plt.title('Répartition des groupes de clusters par groupes Nutrigrade', fontsize=16)
plt.grid(False)
plt.savefig("assets/graphiques/cluster nutrigrade non.jpg")
plt.show()

Ce n'est pas les nutrigrades qui represente les cluster ni le nutriscore D'après l'ACP, le rôle de l'énergie est sighnificatifs ==> on affiche la description de chaque groupe (cluster) en fonction des calories

In [104]:
# Groupe 0
# Condition
mask0 = df_cluster_all['cluster'] == 0
# création d'un dataframe du cluster 0
df0 = df_cluster_all[mask0]['energy_100g']
# On sauvegarde le describe de ce df dans une variable
serie_0 = df0.describe()
# Groupe 1
mask1 = df_cluster_all['cluster'] == 1
df1 = df_cluster_all[mask1]['energy_100g']
serie_1 = df1.describe()
# Groupe 2
mask2 = df_cluster_all['cluster'] == 2
df2 = df_cluster_all[mask2]['energy_100g']
serie_2 = df2.describe()
# df_cluster_all des statistiques par groupe
# On crée une liste contenant nos 3 describe
liste_cluster = [serie_0.values, serie_1.values, serie_2.values]
# index
cols_cluster = serie_1.index
# Création df_cluster_all pour comparer les describe de nos 3 df de cluster
df_cluster_all_energy = pd.DataFrame(liste_cluster, columns=cols_cluster, index=['Groupe 0', 'Groupe 1', 'Groupe 2'])
# On affiche le résultat des groupes
df_cluster_all_energy
Out[104]:
count mean std min 25% 50% 75% max
Groupe 0 80959.0 1679.197405 390.402322 0.0 1464.0 1653.0 1966.0 3736.0
Groupe 1 104530.0 524.453054 366.398999 0.0 222.0 444.0 800.0 2134.0
Groupe 2 32382.0 2008.094869 599.070121 0.0 1494.0 1954.0 2397.0 3776.0
In [105]:
# Groupe 0
# Condition
mask0 = df_cluster_all['cluster'] == 0
# création d'un dataframe du cluster 0
df0 = df_cluster_all[mask0]['nutrition_score_fr_100g']
# On sauvegarde le describe de ce df dans une variable
serie_0 = df0.describe()
# Groupe 1
mask1 = df_cluster_all['cluster'] == 1
df1 = df_cluster_all[mask1]['nutrition_score_fr_100g']
serie_1 = df1.describe()
# Groupe 2
mask2 = df_cluster_all['cluster'] == 2
df2 = df_cluster_all[mask2]['nutrition_score_fr_100g']
serie_2 = df2.describe()
# df_cluster_all des statistiques par groupe
# On crée une liste contenant nos 3 describe
liste_cluster = [serie_0.values, serie_1.values, serie_2.values]
# index
cols_cluster = serie_1.index
# Création df_cluster_all pour comparer les describe de nos 3 df de cluster
df_cluster_all_energy = pd.DataFrame(liste_cluster, columns=cols_cluster, index=['Groupe 0', 'Groupe 1', 'Groupe 2'])
# On affiche le résultat des groupes
df_cluster_all_energy
Out[105]:
count mean std min 25% 50% 75% max
Groupe 0 80959.0 12.712472 9.064519 -12.0 8.0 14.0 20.0 40.0
Groupe 1 104530.0 4.003846 6.458552 -15.0 0.0 2.0 9.0 30.0
Groupe 2 32382.0 16.765086 5.965824 -7.0 13.0 18.0 21.0 37.0

C'est l'équation du nutrition_score_fr

  • groupe 1 (47.9%) : moyenne energie nutritionnelle faible (75% des valeurs <780j (167.19 Calories)) 104456 produits
  • groupe 0 (37.2%) : moyenne energie nutritionnelle moyenne (75% des valeurs <1967j (469.81 Calories)) 80949 produits
  • groupe 2 (14.9%) : moyenne energie nutritionnelle élevé (75% des valeurs <3776j (901.88 Calories)) 32466 produits

BILAN¶


Le jeu de données contient toutes les informations dont nous avons besoin pour apporter une information complémentaire de qualité au personne qui souhaite surveiller leur alimentation en situation de surveillance rénale.


Les variables :

  • Le poids : alimentation saine, équilibrée et plaisir : Nutrigrade
    • 'nutrition-score-fr_100g','energy_100g'
    • 'fat_100g', 'saturated-fat_100g','carbohydrates_100g', 'sugars_100g', 'fiber_100g'
      • évolution de l'application : suivre le régime sur la journée puis la semaine
  • Les indicateurs nécessaires pour informer le consommateur :
    • Sel('salt_100g') et le Sodium('sodium_100g') : sel minéral
    • Proteines ('proteins_100g') : Au stade d’insuffisance rénale les besoins en protéines sont de 0,8g/Kg/j
      • Evolution de l'application : en fonction du poids : l'application demandera le poid et le sexe de la personne pour avoir une indication personnalisée
    • Le phosphore est absent mais le phosphore est surtout présent dans les aliments, lié aux protéines
      • La limitation protéique entraîne déjà une diminution des apports en phosphore.
    • Limiter les additifs : Les additif cible sont présent dans le jeu de données
      • Permet d'alerter sur la présences de ces additifs
    • Evolution de l'application : Proposer des alternatives au produit
      • moins salé, sans additif
      • proposé des recettes
In [106]:
# Recherche des produits répondant au top du projet

cond1 = df_nutriscore['nutrition_grade_fr'] == 'a' # notriscore a
cond2 = df_nutriscore['proteins_100g'] < 5 # peu de protéines
cond3 = df_nutriscore['sodium_100g'] < 0.2 # peu de sodium

df_optimal = df_nutriscore.loc[cond1 & cond2 & cond3, ['product_name', 'proteins_100g',
                                                       'sodium_100g','salt_100g',
                                                       'nutrition_grade_fr']] \
.sort_values(['proteins_100g','sodium_100g','salt_100g', 'nutrition_grade_fr'])
df_optimal.sample(10).style.hide_index()
Out[106]:
product_name proteins_100g sodium_100g salt_100g nutrition_grade_fr
Tomato Puree 1.590000 0.063000 0.160020 a
Hot Sauce 0.000000 0.041000 0.104140 a
Crinkle Cut Carrots 1.180000 0.071000 0.180340 a
Sliced Carrots 0.830000 0.046000 0.116840 a
Fresh Juice, Pineapple Kiwi Pear 0.420000 0.000000 0.000000 a
Skim Grade A Fat Free Milk 3.330000 0.050000 0.127000 a
Pommes & Châtaignes 0.500000 0.003543 0.009000 a
Oranges à dessert lane late, U, calibre 5/6, catégorie 1, Espagne, filet 0.900000 0.000000 0.000000 a
Half & Half Blend Baby Spinach & Spring Mix 2.350000 0.071000 0.180340 a
Base pour sauce tomate basilic 1.500000 0.137795 0.350000 a
In [109]:
cols = ['product_name','proteins_100g','sodium_100g','additives_fr','nutrition_grade_fr' ]
x = df_nutriscore.loc[df_nutriscore['code']=='7613035336544',cols]
x.style.hide_index()
Out[109]:
product_name proteins_100g sodium_100g additives_fr nutrition_grade_fr
Alumettes jambon herta 17.100000 0.849000 E250 - Nitrite de sodium,E326 - Lactate de potassium,E262 - Acétates de sodium,E316 - Erythorbate de sodium e
In [108]:
df_nutriscore.columns
Out[108]:
Index(['code', 'creator', 'created_datetime', 'last_modified_datetime',
       'product_name', 'brands', 'categories_fr', 'countries_fr',
       'additives_n', 'additives_fr', 'ingredients_from_palm_oil_n',
       'nutrition_grade_fr', 'main_category_fr', 'energy_100g', 'fat_100g',
       'saturated_fat_100g', 'carbohydrates_100g', 'sugars_100g', 'fiber_100g',
       'proteins_100g', 'salt_100g', 'sodium_100g', 'nutrition_score_fr_100g'],
      dtype='object')